/**
 * Copyright (c) 2011 - 2012 National Library of Medicine
 *
 * Contact: tomasz.adamusiak@nih.gov
 * 
 * This file is part of LOwlINC
 * 
 * LOwlINC is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option) any
 * later version.
 * 
 * LOwlINC is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along
 * with LOwlINC. If not, see <http://www.gnu.org/licenses/>.
 */
package gov.nih.nlm.lowlinc.core;

import gov.nih.nlm.lowlinc.accessionwrapper.LOINCID;
import gov.nih.nlm.lowlinc.accessionwrapper.LOINCPartID;
import gov.nih.nlm.lowlinc.accessionwrapper.LOINCTermID;
import gov.nih.nlm.lowlinc.accessionwrapper.SNOMEDTermID;
import gov.nih.nlm.lowlinc.bean.LOINCPart;
import gov.nih.nlm.lowlinc.bean.LOINCTerm;
import gov.nih.nlm.lowlinc.csv.AxialGraph;
import gov.nih.nlm.lowlinc.csv.LOINCAxialCSVParser;
import gov.nih.nlm.lowlinc.csv.LOINCPartLinksCSVParser;
import gov.nih.nlm.lowlinc.csv.LOINCPartsCSVParser;
import gov.nih.nlm.lowlinc.csv.LOINCTermsCSVParser;
import gov.nih.nlm.lowlinc.csv.MrConsoCSVParser;
import gov.nih.nlm.lowlinc.owlapi.LowlincException;
import gov.nih.nlm.lowlinc.owlapi.LowlincProgressMonitor;
import gov.nih.nlm.lowlinc.owlapi.Ontologies;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import org.apache.log4j.Logger;
import org.semanticweb.HermiT.Reasoner;
import org.semanticweb.owlapi.model.OWLAxiom;
import org.semanticweb.owlapi.model.OWLClass;
import org.semanticweb.owlapi.reasoner.InferenceType;
import org.semanticweb.owlapi.reasoner.Node;
import org.semanticweb.owlapi.reasoner.OWLReasoner;
import org.semanticweb.owlapi.reasoner.OWLReasonerConfiguration;
import org.semanticweb.owlapi.reasoner.OWLReasonerFactory;
import org.semanticweb.owlapi.reasoner.ReasonerProgressMonitor;
import org.semanticweb.owlapi.reasoner.SimpleConfiguration;
import org.semanticweb.owlapi.util.InferredAxiomGenerator;
import org.semanticweb.owlapi.util.InferredOntologyGenerator;

/**
 * @author Tomasz
 * 
 */
public class LowlincCreator {
	private static final Logger log = Logger.getLogger(LowlincCreator.class);

	private static Map<LOINCTermID, List<LOINCPartID>> partLinks;
	private static Map<LOINCPartID, LOINCPart> parts;
	private static Map<LOINCTermID, LOINCTerm> terms;
	private static AxialGraph axialGraph;

	// creates only a small subset for debugging purposes
	private static boolean shortcut = false;

	/**
	 * @param args
	 * @throws LowlincException
	 */
	public static void main(String[] args) throws LowlincException {
		Ontologies.createOntology(shortcut); // shortcut
		// creator.removeAnnotations();

		// parse multiaxial
		axialGraph = new LOINCAxialCSVParser().getGraph();
		partLinks = new LOINCPartLinksCSVParser().getLinks();
		parts = new LOINCPartsCSVParser().getParts();
		terms = new LOINCTermsCSVParser().getTerms();

		// matches missing links and adds them to partLink
		LowlincValidator.findAndAddMissingPartLinks(partLinks, parts, terms);
		// decomposes multiaxial hierarchy parts into observations
		MultiaxialComposer.decomposeParts(axialGraph, parts, partLinks);

		addTerms();
		addAxialObservations();
		addAxial();
		addParts();
		// addDummyRestrictions(creator);
		if (!shortcut) {
			addSnomedLinks();
		}

		addSiblingDisjoints();

		Ontologies.saveOntology(LowlincConfig.getString("SAVE_PATH"));
		log.info("SAVED regular");
		Ontologies.saveOntologyInFunctional(LowlincConfig
				.getString("FUNCTIONAL_SAVE_PATH"));
		log.info("SAVED functional");
		classifyOntology();

	}

	private static void addSiblingDisjoints() throws LowlincException {
		log.info("Adding sibling disjoints");
		Ontologies.addSiblingDisjoints();
	}

	private static void addSnomedLinks() throws LowlincException {
		Map<LOINCPartID, Set<SNOMEDTermID>> loincLinks = new MrConsoCSVParser()
				.getLOINCmaps();
		Ontologies.addSnomedEquivalences(loincLinks);
		log.info("Added SNOMED - LOINC parts and properties equivalences");
	}

	private static void addDummyRestrictions() throws LowlincException {
		// find class parts
		Set<LOINCPart> classParts = new HashSet<>();
		for (LOINCPart bean : parts.values()) {
			if (bean.getPART_TYPE().equalsIgnoreCase("CLASS")) {
				classParts.add(bean);
			}
		}
		log.info(classParts.size() + " class parts loaded");
		Ontologies.addDummyClassParents(classParts);
		log.info("CLASS disjoints and dummy terms added");
	}

	private static void classifyOntology() throws LowlincException {

		// HermiT
		OWLReasonerFactory reasonerFactory = new Reasoner.ReasonerFactory();

		// FactPP
		// OWLReasonerFactory reasonerFactory = new
		// FaCTPlusPlusReasonerFactory();

		ReasonerProgressMonitor progressMonitor = new LowlincProgressMonitor();

		OWLReasonerConfiguration config = new SimpleConfiguration(
				progressMonitor);

		OWLReasoner reasoner = reasonerFactory.createReasoner(
				Ontologies.getOntology(), config);

		// Ask the reasoner to do all the necessary work now
		reasoner.precomputeInferences(InferenceType.CLASS_HIERARCHY);

		// We can determine if the ontology is actually consistent (in this
		// case, it should be).
		boolean consistent = reasoner.isConsistent();
		log.info("Consistent: " + consistent);

		Node<OWLClass> bottomNode = reasoner.getUnsatisfiableClasses();
		Set<OWLClass> unsatisfiable = bottomNode.getEntitiesMinusBottom();
		if (!unsatisfiable.isEmpty()) {
			log.warn("The following classes are unsatisfiable: ");
			for (OWLClass cls : unsatisfiable) {
				log.info("UNSATISFIABLE\t" + cls);
			}
		} else {
			log.info("There are no unsatisfiable classes");
		}

		// Docs suggest using inferredaxiomgenerators here, but can also use
		// implied default

		List<InferredAxiomGenerator<? extends OWLAxiom>> axiomGenerators = new ArrayList<>();
		// axiomGenerators.add(new InferredClassAssertionAxiomGenerator());
		// axiomGenerators.add(new InferredEquivalentClassAxiomGenerator());
		// axiomGenerators.add(new InferredSubClassAxiomGenerator());
		// axiomGenerators.add(new
		// InferredEquivalentObjectPropertyAxiomGenerator());
		// axiomGenerators.add(new InferredSubObjectPropertyAxiomGenerator());

		// Now get the inferred ontology generator to generate the inferred view
		// ignore axiom generators
		InferredOntologyGenerator iog = new InferredOntologyGenerator(reasoner);

		// save original axioms
		// Set<OWLAxiom> assertedAxioms = creator.getOntology().getAxioms();

		log.info("Filling ontology with InferredOntolgoyGenerator");
		// create an empty one
		Ontologies.createOntology(true);
		iog.fillOntology(Ontologies.getManager(), Ontologies.getOntology());

		// Set<OWLAxiom> inferredAxioms = creator.getOntology().getAxioms();

		// log.info("Number of asserted axioms: " + assertedAxioms.size());
		// inferredAxioms.removeAll(assertedAxioms);
		// log.info("Number of inferred axioms: " + inferredAxioms.size());
		// Integer subclassInferred = 0;
		// Integer equivalentInferred = 0;
		// for (OWLAxiom axiom : inferredAxioms) {
		// if (axiom.getAxiomType().equals(AxiomType.SUBCLASS_OF)) {
		// subclassInferred++;
		// }
		// if (axiom.getAxiomType().equals(AxiomType.EQUIVALENT_CLASSES)) {
		// equivalentInferred++;
		// }
		// }
		// log.info("Number of inferred SUBCLASS_OF axioms: " +
		// subclassInferred);
		// log.info("Number of inferred EQUIVALENT_CLASSES axioms: "
		// + equivalentInferred);
		//
		// OWLAxiom[] listAxioms = inferredAxioms.toArray(new OWLAxiom[0]);
		// Arrays.sort(listAxioms, new OWLAxiomComparator());
		//
		// for (OWLAxiom axiom : listAxioms) {
		// log.info("Inferred axiom: "
		// + creator.replaceIDsWithLabels(axiom.toString()));
		// }

		// Save the inferred ontology.
		log.info("Saving inferred ontology");
		Ontologies.saveOntology(LowlincConfig.getString("INFERRED_SAVE_PATH"));
	}

	private static void addAxial() throws LowlincException {

		Integer partCounter = 0;
		Integer termCounter = 0;

		// this creates the multiaxial parts under part
		for (LOINCID partID : axialGraph.getNodes()) {
			// ignore terms here
			// term axial info is added by multi axial relation from the part
			// links file
			if (partID instanceof LOINCTermID) {
				termCounter = termCounter
						+ axialGraph.getParents(partID).size();
				continue;
			}
			for (LOINCID parentID : axialGraph.getParents(partID)) {
				if (parentID instanceof LOINCTermID) {
					throw new LowlincException(
							"related terms in Axial hierarchy");
				}
				OWLClass cls = Ontologies.createLOINCClass(partID);
				// shortcut
				if (shortcut && Ontologies.isNewEntity(cls.getIRI())) {
					continue;
				}
				partCounter++;
				OWLClass parent = Ontologies.createLOINCClass(parentID);
				Ontologies.addParent(cls, parent);
			}

		}
		log.info("Added " + partCounter + " part is_a relations");
		log.info("Seen "
				+ termCounter
				+ " possible term has_multiaxial relations in multi-axial_hierarchy.csv to be added later through partlinks");
	}

	private static void addAxialObservations() throws LowlincException {
		// shortcut other MICRO
		if (shortcut) {
			return;
		}

		// iterate over all axial parts
		for (LOINCPartID partID : axialGraph.getParts()) {
			LOINCTermID termID = new LOINCTermID("OBS_" + partID.getID());
			LOINCPart part = parts.get(partID);

			// create observation class
			OWLClass cls = Ontologies.createLOINCClass(termID);
			Ontologies.addTermsParent(cls);
			Ontologies.addLabel(cls, "OBS " + part.getLabel());
			Ontologies.addAxialObservation(cls, part.getPartComposition());
		}
		log.info("Added " + axialGraph.getParts().size()
				+ " axial observations");
	}

	private static void addParts() throws LowlincException {

		Integer counter = 0;
		for (LOINCPart part : parts.values()) {
			OWLClass cls = Ontologies.createLOINCClass(part.getPartID());

			// shortcut
			if (shortcut && Ontologies.isNewEntity(cls.getIRI())) {
				continue;
			}
			counter++;

			// create class
			Ontologies.addLabel(cls, part.getLabel());

			// add as a isa_relation
			Ontologies.addPartsParent(cls);
			// copy metadata
			// copyMetadata(creator, cls, bean);
		}

		log.info("Added " + counter + " parts");
	}

	private static void addTerms() throws LowlincException {

		Map<LOINCPartID, Integer> missingParts = new HashMap<>();
		Integer counter = 0;
		Integer axialCounter = 0;

		for (LOINCTerm term : terms.values()) {

			LOINCTermID termID = term.getTermID();
			// RELATIONS
			Map<LOINCPartID, List<String>> relations = new HashMap<>();

			// add axial relations
			if (axialGraph.getTerms().contains(termID)) {
				for (LOINCID partID : axialGraph.getParents(termID)) {
					String partType = "MULTI-AXIAL";
					axialCounter++;
					if (!relations.containsKey(partID)) {
						List<String> types = new ArrayList<>();
						types.add(partType);
						relations.put((LOINCPartID) partID, types);
					} else {
						relations.get(partID).add(partType);
						log.warn(termID + " duplicated axial relations "
								+ relations.get(partID) + " to " + partID);
					}

				}
			}
			// add part links relations
			for (LOINCPartID partID : partLinks.get(termID)) {
				LOINCPart part = parts.get(partID);
				if (part != null) {
					String partType = parts.get(partID).getPART_TYPE();
					if (!relations.containsKey(partID)) {
						List<String> types = new ArrayList<>();
						types.add(partType);
						relations.put(partID, types);
					} else {
						relations.get(partID).add(partType);
						log.debug(termID + " duplicated relations "
								+ relations.get(partID) + " to " + partID);
					}
				} else {
					if (missingParts.containsKey(partID)) {
						missingParts.put(partID, missingParts.get(partID) + 1);
					} else {
						missingParts.put(partID, 1);
					}
				}
			}

			// see if relations are reused
			Map<String, List<LOINCPartID>> usage = new HashMap<>();
			for (LOINCPartID partID : relations.keySet()) {
				for (String partType : relations.get(partID)) {
					if (usage.containsKey(partType)) {
						usage.get(partType).add(partID);
					} else {
						List<LOINCPartID> parts = new ArrayList<>();
						parts.add(partID);
						usage.put(partType, parts);
					}
				}
			}
			for (String partType : usage.keySet()) {
				List<LOINCPartID> parts = usage.get(partType);
				if (parts.size() > 1) {
					log.debug(termID + " reuses " + partType + " for " + parts);
				}
			}

			// shortcut other MICRO
			if (shortcut && !term.getCLASS().startsWith("EYE")) {
				continue;
			}
			counter++;

			// create class
			OWLClass cls = Ontologies.createLOINCClass(termID);
			Ontologies.addLabel(cls, term.getLabel());

			if (term.isActive()) {
				// add as a isa_relation
				Ontologies.addTermsParent(cls);

				// creator.addAnnoation(cls, "COMPONENT", term.getCOMPONENT());
				// creator.addAnnoation(cls, "PROPERTY", term.getPROPERTY());
				// creator.addAnnoation(cls, "TIME_ASPCT",
				// term.getTIME_ASPCT());
				// creator.addAnnoation(cls, "SYSTEM", term.getSYSTEM());
				// creator.addAnnoation(cls, "SCALE_TYP", term.getSCALE_TYP());
				// creator.addAnnoation(cls, "METHOD_TYP",
				// term.getMETHOD_TYP());
				// if (!term.getCOMMON_TEST_RANK().equals("0")) {
				// creator.addAnnoation(cls, "COMMON_TEST_RANK",
				// term.getCOMMON_TEST_RANK());
				// }

				if (relations.size() > 0) {
					Ontologies.addRelations(cls, relations, term.getIndex());
				} else {
					log.warn("No part links for " + termID);
				}

			} else {
				Ontologies.addDeprecatedParent(cls);
				Ontologies.addAnnoation(cls, "STATUS", term.getSTATUS());

				if (term.getMAP_TO() != null) {
					OWLClass mapTo = Ontologies.createLOINCClass(term
							.getMAP_TO());
					Ontologies.addSubclassMapsTo(cls, mapTo);
				}

			}

			// copy metadata
			// this wil single handedly blow up the size of the ontology to
			// 400MB
			// copyMetadata(creator, cls, term);

		}

		log.info("Added " + counter + " terms");
		log.info("Added " + axialCounter + " has_multiaxial relations");

		// print collated warnings
		printSortedWarnings(missingParts);
	}

	private static void printSortedWarnings(
			Map<LOINCPartID, Integer> missingParts) {
		ValueComparator vc = new ValueComparator(missingParts);
		TreeMap<LOINCPartID, Integer> sorted_map = new TreeMap<>(vc);
		sorted_map.putAll(missingParts);
		for (Entry<LOINCPartID, Integer> warning : sorted_map.entrySet()) {
			log.warn("Missing " + warning.getKey() + " used in part links "
					+ warning.getValue() + " times");
		}

	}

	public static class ValueComparator implements Comparator<LOINCPartID> {
		private final Map<LOINCPartID, Integer> base;

		public ValueComparator(Map<LOINCPartID, Integer> base) {
			this.base = base;
		}

		@Override
		public int compare(LOINCPartID a, LOINCPartID b) {
			if (base.get(a) == base.get(b) || base.get(a) < base.get(b)) {
				return 1;
			} else {
				return -1;
			}
		}
	}

	@SuppressWarnings("unused")
	private static void copyMetadata(Ontologies creator, OWLClass cls,
			Object bean) throws LowlincException {
		// use reflection
		for (Method method : bean.getClass().getMethods()) {
			if (method.getName().startsWith("get")
					&& method.getReturnType().equals(String.class)) {
				String accession = method.getName().replaceFirst("get", "");
				try {
					String value = (String) method.invoke(bean);
					Ontologies.addAnnoation(cls, accession, value);
				} catch (IllegalAccessException | IllegalArgumentException
						| InvocationTargetException e) {
					throw new LowlincException(e);
				}
			}

		}
	}

}
